/**********************************************************************\

 Copyright 2012 Diggory Blake

 This file is part of klip-engine

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

\**********************************************************************/

#include "kgraphicsdx.h"
#include "kimagedx.h"
#include "kerror.h"
#include "kengine.h"
#include "kdata.h"
#include "kview.h"
#include "ksprite.h"
#include "kcamera.h"
#include "kperformance.h"
#include "ksystemwindows.h"
#include "kimagefile.h"
#include "kstream.h"
#include "keffectcg.h"
#include "kmaterial.h"
#include "kimagecache.h"

const tchar* kGraphicsDX::_semanticNames[kGraphicsDX::ES_Count] = {
	_T("View"),
	_T("Projection"),
	_T("ViewProjection"),
	_T("InvView"),
	_T("InvProjection"),
	_T("InvViewProjection")
};
void kGraphicsDX::createImageDX(const kVec2i& size, keImageOptions options, IDirect3DTexture9** texture, IDirect3DSurface9** surface) {
	DWORD usage = (options & keImage_RenderTarget) ? D3DUSAGE_RENDERTARGET : 0;
	D3DPOOL pool = (options & keImage_RenderTarget) ? D3DPOOL_DEFAULT : D3DPOOL_MANAGED;
	D3DFORMAT fmt = (options & keImage_Opaque) ? D3DFMT_R8G8B8 : D3DFMT_A8R8G8B8;
	bool drawable = (options & keImage_Drawable) == keImage_Drawable;

	if (drawable) {
		*surface = 0;
		D3DXCreateTexture(_device, size.x, size.y, 1, usage, fmt, pool, texture);
		if (!*texture)
			throw kError(_T("Failed to create image!"));

		(*texture)->GetSurfaceLevel(0, surface);
	} else {
		*texture = 0;
		_device->CreateOffscreenPlainSurface(size.x, size.y, fmt, pool, surface, 0);
		if (!*surface)
			throw kError(_T("Failed to create image!"));
	}
}
void kGraphicsDX::resetDevice() {
	HRESULT status = _device->TestCooperativeLevel();
	if (status != D3DERR_DEVICENOTRESET && status != D3D_OK)
		return;

	for (auto i = _images.begin(); i != _images.end(); i++)
		(*i)->resetDevice(false);

	D3DPRESENT_PARAMETERS pp;
	fillPresentParameters(&pp);

	_device->Reset(&pp);

	if (_device->TestCooperativeLevel() != D3D_OK)
		throw kError(_T("Could not reset device!"));

	for (auto i = _images.begin(); i != _images.end(); i++)
		if (*i != _backBuffer)
			(*i)->resetDevice(true);
	
	_backBuffer->_size = _size;
	_device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &_backBuffer->_surface);
	if (!_backBuffer->_surface)
		throw kError(_T("Failed to access backbuffer!"));
}
void kGraphicsDX::fillPresentParameters(D3DPRESENT_PARAMETERS* pp) {
	bool fullscreen = (_options & keWindow_Fullscreen) == keWindow_Fullscreen;

	memset(pp, 0, sizeof(D3DPRESENT_PARAMETERS));
	pp->BackBufferCount = 1;
	pp->BackBufferFormat = fullscreen ? D3DFMT_X8R8G8B8 : D3DFMT_UNKNOWN;
	pp->BackBufferHeight = _size.y;
	pp->BackBufferWidth = _size.x;
	pp->FullScreen_RefreshRateInHz = fullscreen ? 60 : 0;
	pp->hDeviceWindow = (HWND)_window->getHandle();
	pp->PresentationInterval = D3DPRESENT_INTERVAL_ONE;
	pp->SwapEffect = D3DSWAPEFFECT_DISCARD;
	pp->Windowed = !fullscreen;
}
kGraphicsDX::kGraphicsDX(kSystemWindows* system, kWindow* window) {
	_system = system;
	_window = window;
	_direct3d = 0;
	_device = 0;
	_backBuffer = 0;
	_size = _window->getSize();
	_options = _window->getOptions();

	_resizeCallback = _window->onResize.attach(
		[this](kHandle window, const kVec2i& size) {
		_size = size;
		resetDevice();
	});
	_changeCallback = _window->onChange.attach(
		[this](kHandle window, keWindowOptions options) {
		_options = options;
		resetDevice();
	});
	_deleteCallback = _window->onDestroy.attach(
		[this](kHandle window, bool* quit) {
		_resizeCallback = 0;
		_changeCallback = 0;
		_deleteCallback = 0;
	});

	_direct3d = Direct3DCreate9(D3D_SDK_VERSION);
	if (!_direct3d)
		throw kError(_T("Failed to create Direct3D!"));

	D3DPRESENT_PARAMETERS pp;
	fillPresentParameters(&pp);

	HRESULT hr = _direct3d->CreateDevice(0, D3DDEVTYPE_HAL, (HWND)_window->getHandle(), D3DCREATE_HARDWARE_VERTEXPROCESSING|D3DCREATE_NOWINDOWCHANGES, &pp, &_device);
	if (FAILED(hr)) {
		throw kError(_T("Failed to create Direct3D device!"));
	}

	IDirect3DSurface9* backBuffer = 0;
	_device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backBuffer);
	if (!backBuffer)
		throw kError(_T("Failed to access backbuffer!"));

	_backBuffer = new kImageDX(this, backBuffer);
	backBuffer->Release();

	_cgContext = cgCreateContext();
	cgD3D9SetDevice(_device);
	cgD3D9RegisterStates(_cgContext);
	cgD3D9SetManageTextureParameters(_cgContext, true);

	_imageCache = new kImageCache(this);
}
void kGraphicsDX::onUpdate(float delta) {
	PERF_QUERY;

	HRESULT result = _device->Present(0, 0, 0, 0);
	if (result == D3DERR_DEVICELOST) {
		resetDevice();
	}
}
kImage* kGraphicsDX::createImage(kStream* source, keImageOptions options) {
	IDirect3DTexture9* texture = 0;
	//D3DFORMAT fmt = (options & keImage_Opaque) ? D3DFMT_R8G8B8 : D3DFMT_A8R8G8B8;
	//D3DXCreateTextureFromFileInMemoryEx(
	//	_device, source->getPtr(), source->getSize(),
	//	D3DX_FROM_FILE, D3DX_FROM_FILE, 1, 0, fmt, D3DPOOL_MANAGED,
	//	D3DX_FILTER_NONE, D3DX_DEFAULT, 0, 0, 0, &texture);

	kImageHeader header;
	header.format = keImage_Unknown;
	header.width = 0;
	header.height = 0;
	header.depth = (options & keImage_Opaque) ? 3 : 4;
	D3DFORMAT fmt = (header.depth == 3) ? D3DFMT_R8G8B8 : D3DFMT_A8R8G8B8;

	size_t pos = source->tell(keSeek_Read);
	if (!kImageFile::load(source, &header, 0))
		return 0;

	D3DXCreateTexture(_device, header.width, header.height, 1, 0, fmt, D3DPOOL_MANAGED, &texture);
	D3DLOCKED_RECT rc;
	texture->LockRect(0, &rc, 0, D3DLOCK_DISCARD);
	
	source->seek(keSeek_Read, pos);
	kImageFile::load(source, &header, [&](uint32_t y, const uint8_t* data) {
		uint8_t* dest = (uint8_t*)rc.pBits + y*rc.Pitch;
		memcpy(dest, data, header.width*header.depth);
	});

	texture->UnlockRect(0);

	if (!texture)
		throw kError(_T("Could not create image!"));

	kImageDX* result = new kImageDX(this, texture);
	texture->Release();

	return result;
}
void WINAPI kGraphicsDX_ColorFill (D3DXVECTOR4* pOut, const D3DXVECTOR2* pTexCoord, const D3DXVECTOR2* pTexelSize, void* pData) {
    const kColor* color = (const kColor*)pData;
	pOut->w = color->a;
	pOut->x = color->r;
	pOut->y = color->g;
	pOut->z = color->b;
}
kImage* kGraphicsDX::createImage(const kVec2i& size, const kColor& color, keImageOptions options) {
	DWORD usage = (options & keImage_RenderTarget) ? D3DUSAGE_RENDERTARGET : 0;
	D3DPOOL pool = (options & keImage_RenderTarget) ? D3DPOOL_DEFAULT : D3DPOOL_MANAGED;
	D3DFORMAT fmt = (options & keImage_Opaque) ? D3DFMT_R8G8B8 : D3DFMT_A8R8G8B8;

	IDirect3DTexture9* texture = 0;
	IDirect3DSurface9* surface = 0;
	createImageDX(size, options, &texture, &surface);
	kImageDX* result = 0;

	if (texture) {
		D3DXFillTexture(texture, &kGraphicsDX_ColorFill, (void*)&color);

		result = new kImageDX(this, texture);
		texture->Release();
	} else if (surface) {
		_device->ColorFill(surface, 0, color.toArgb());
		
		result = new kImageDX(this, surface);
		surface->Release();
	}
	
	return result;
}
void kGraphicsDX::deleteImage(kImage* image) {
	delete image;
}
kGraphicsDX::~kGraphicsDX() {
	if (_imageCache) {
		delete _imageCache;
		_imageCache = 0;
	}

	if (_resizeCallback)
		_window->onResize.detach(_resizeCallback);
	if (_changeCallback)
		_window->onChange.detach(_changeCallback);
	if (_deleteCallback)
		_window->onDestroy.detach(_deleteCallback);

	if (_cgContext) {
		cgDestroyContext(_cgContext);
		cgD3D9SetDevice(0);
		cgD3D9RegisterStates(_cgContext);
	}
	if (_device) {
		_device->Release();
		_device = 0;
	}
	if (_direct3d) {
		_direct3d->Release();
		_direct3d = 0;
	}
}
kImage* kGraphicsDX::getBackBuffer() {
	return _backBuffer;
}
void kGraphicsDX::beginRender(kView* view, bool resume) {
	PERF_QUERY;

	_currentView = view;

	kImage* renderTarget = view->getRenderTarget();
	if (!renderTarget)
		return;

	HRESULT hr = _device->SetRenderTarget(0, renderTarget->as<kImageDX*>()->getSurfaceDX());
	if (FAILED(hr))
		throw kError(_T("Failed to set render target!"));

	D3DVIEWPORT9 viewport;
	const kRect& rect = view->getViewport();
	viewport.X = rect.left;
	viewport.Y = rect.top;
	viewport.Width = rect.width;
	viewport.Height = rect.height;
	viewport.MinZ = 0.0f;
	viewport.MaxZ = 1.0f;

	_device->SetViewport(&viewport);

	kCamera* camera = view->getCamera();

	D3DXMatrixOrthoOffCenterLH(&_semanticValues[ES_Projection], viewport.X + 0.5f, viewport.X + viewport.Width + 0.5f, viewport.Y+viewport.Height + 0.5f, viewport.Y + 0.5f, 0.0f, 1.0f);
	_device->SetTransform(D3DTS_PROJECTION, &_semanticValues[ES_Projection]);
	D3DXMatrixInverse(&_semanticValues[ES_InvProjection], 0, &_semanticValues[ES_Projection]);

	if (camera) {
		getMatrixDX(camera->getTransform(), &_semanticValues[ES_InvView]);
		getMatrixDX(camera->getInvTransform(), &_semanticValues[ES_View]);
	} else {
		D3DXMatrixIdentity(&_semanticValues[ES_InvView]);
		D3DXMatrixIdentity(&_semanticValues[ES_View]);
	}
	_device->SetTransform(D3DTS_VIEW, &_semanticValues[ES_View]);
	D3DXMatrixMultiply(&_semanticValues[ES_ViewProjection], &_semanticValues[ES_View], &_semanticValues[ES_Projection]);
	D3DXMatrixMultiply(&_semanticValues[ES_InvViewProjection], &_semanticValues[ES_InvProjection], &_semanticValues[ES_InvView]);

	_device->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
	_device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
	_device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);
	_device->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
	_device->SetRenderState(D3DRS_LASTPIXEL, TRUE);
	_device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
	_device->SetRenderState(D3DRS_DITHERENABLE, FALSE);
	_device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
	_device->SetRenderState(D3DRS_FOGENABLE, FALSE);
	_device->SetRenderState(D3DRS_SPECULARENABLE, FALSE);
	_device->SetRenderState(D3DRS_STENCILENABLE, FALSE);
	_device->SetRenderState(D3DRS_TEXTUREFACTOR, 0xFFFFFFFF);
	_device->SetRenderState(D3DRS_WRAP0, 0);
	_device->SetRenderState(D3DRS_CLIPPING, TRUE);
	_device->SetRenderState(D3DRS_LIGHTING, FALSE); //
	_device->SetRenderState(D3DRS_COLORVERTEX, TRUE);
	_device->SetRenderState(D3DRS_ANTIALIASEDLINEENABLE, TRUE);
	_device->SetRenderState(D3DRS_BLENDFACTOR, 0xFFFFFFFF);
	_device->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
	_device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
	_device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
	_device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
	_device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
	_device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
	_device->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);

	if (view->getBackdropEnabled() && !resume) {
		kColor backdrop = clamp(view->getBackdrop());
		_device->Clear(0, 0, D3DCLEAR_TARGET, backdrop.toArgb(), 0.0f, 0);
	}

	_device->BeginScene();
}
void kGraphicsDX::endRender() {
	PERF_QUERY;

	while (_drawBuffers.size())
		flush(_drawBuffers.size()-1);

	_device->EndScene();
	_device->SetRenderTarget(0, 0);

	_currentView = 0;
}
void kGraphicsDX::renderMesh(kMesh2DBase* mesh) {
	PERF_QUERY;

	const kAABB& aabb = mesh->getAABB();

	size_t mappingCount = mesh->getMappingCount();
	size_t colorStages = std::min(mesh->getColorStages(), 2u);
	size_t textureStages = std::min(mesh->getTextureStages(), 8u);
	size_t vertexSize = 12 + colorStages*4 + textureStages*8;

	DrawBuffer* outBuffer = 0;
	DrawState state;
	state.fvf = D3DFVF_XYZ|(colorStages*D3DFVF_DIFFUSE)|(textureStages*D3DFVF_TEX1);
	state.repeatX = mesh->getMaterialRepeatX();
	state.repeatY = mesh->getMaterialRepeatY();
	state.material = mesh->getMaterial();
	switch (mesh->getMeshType()) {
	case keMesh_PointList:
		state.primType = D3DPT_POINTLIST;
		break;
	case keMesh_LineStrip:
	case keMesh_LineLoop:
	case keMesh_LineList:
		state.primType = D3DPT_LINELIST;
		break;
	case keMesh_TriangleFan:
	case keMesh_TriangleList:
		state.primType = D3DPT_TRIANGLELIST;
		break; 
	}
	state.blend = mesh->getBlend();

	for (unsigned i = 0; i < _drawBuffers.size(); i++) {
		DrawBuffer& buffer = *_drawBuffers[i];
		if (buffer.state == state) {
			if (buffer.vertexData.getItemCount() < buffer.vertexCount + mappingCount) {
				flush(i--);
			} else {
				outBuffer = &buffer;
			}
		} else if (buffer.aabb.intersects(aabb)) {
			flush(i--);
		}
	}
	if (!outBuffer) {
		_drawBuffers.push_back(new DrawBuffer());
		outBuffer = _drawBuffers.back();
		outBuffer->vertexCount = 0;
		outBuffer->state = state;
		outBuffer->vertexData.replace(vertexSize, std::max(4096u, mappingCount*2u));
	}

	outBuffer->aabb = outBuffer->aabb.add(aabb);
	
	size_t startIndex = outBuffer->vertexCount;
	outBuffer->vertexCount += mappingCount;

	for (size_t i = 0; i < mappingCount; i++) {
		size_t vertex = mesh->getMapping(i);
		size_t offset = 0;
		size_t index = startIndex + i;

		// POSITION
		outBuffer->vertexData.fieldInc<kVec3>(index, offset) = kVec3(mesh->getTransformedVertex(vertex));

		// COLOR[i]
		for (size_t j = 0; j < colorStages; j++) {
			outBuffer->vertexData.fieldInc<D3DCOLOR>(index, offset) = mesh->getColor(vertex, j).toArgb();
		}

		// TEXCOORD[i]
		for (size_t j = 0; j < textureStages; j++) {
			outBuffer->vertexData.fieldInc<kVec2>(index, offset) = mesh->getTextureCoord(vertex, j);
		}
	}
}
void kGraphicsDX::drawBuffer(const DrawBuffer& buffer) {
	PERF_QUERY;

	size_t vertexCount = buffer.vertexCount;
	size_t primCount = 0;
	switch (buffer.state.primType) {
	case D3DPT_POINTLIST:
		primCount = vertexCount;
		break;
	case D3DPT_LINELIST:
		primCount = vertexCount/2;
		break;
	case D3DPT_TRIANGLELIST:
		primCount = vertexCount/3;
		break;
	}

	_device->DrawPrimitiveUP(buffer.state.primType, primCount, buffer.vertexData.getPtr(), buffer.vertexData.getItemSize());
}
void kGraphicsDX::flush(unsigned bufferIndex) {
	PERF_QUERY;

	DrawBuffer& buffer = *_drawBuffers[bufferIndex];
	DrawState& state = buffer.state;
	
	_device->SetFVF(state.fvf);
	_device->SetRenderState(D3DRS_SRCBLEND, convertBlendFactor(state.blend.srcBlend));
	_device->SetRenderState(D3DRS_DESTBLEND, convertBlendFactor(state.blend.dstBlend));
	_device->SetRenderState(D3DRS_BLENDOP, convertBlendOp(state.blend.blendOp));
	if (buffer.state.blend.separateAlphaBlend()) {
		_device->SetRenderState(D3DRS_SRCBLENDALPHA, convertBlendFactor(state.blend.srcBlendAlpha));
		_device->SetRenderState(D3DRS_DESTBLENDALPHA, convertBlendFactor(state.blend.dstBlendAlpha));
		_device->SetRenderState(D3DRS_BLENDOPALPHA, convertBlendOp(state.blend.blendOpAlpha));
		
		_device->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, TRUE);
	} else {
		_device->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE);
	}

	if (state.material) {
		kEffectCg* effect = state.material->getEffect()->as<kEffectCg*>();
		state.material->begin();

		if (effect) {
			for (auto i = effect->getSemantics().begin(); i != effect->getSemantics().end(); i++) {
				if (i->first < ES_Count) {
					effect->setParam(i->second, 16, _semanticValues[i->first]);
				}
			}

			for (size_t i = 0; i < 8; i++) {
				CGparameter param = effect->getTextureParam(i);
				if (param) {
					const kImageDX* texture = state.material->getTexture(i)->as<kImageDX*>();
					cgD3D9SetTextureParameter(param, texture ? texture->getTextureDX() : 0);
				}
			}

			CGeffect cgEffect = effect->getEffectCg();

			CGtechnique tech = effect->getTechniqueCg();
			for (CGpass pass = cgGetFirstPass(tech); pass; pass = cgGetNextPass(pass)) {
				cgSetPassState(pass);
				drawBuffer(buffer);
				cgResetPassState(pass);
			}
		} else {
			D3DXMATRIX mat;
			D3DXMatrixIdentity(&mat);
			_device->SetTransform(D3DTS_WORLD, &mat);
			_device->SetVertexShader(0);
			_device->SetPixelShader(0);

			for (size_t i = 0; i < 8; i++) {
				const kImageDX* texture = state.material->getTexture(i)->as<kImageDX*>();
				_device->SetTexture(i, texture ? texture->getTextureDX() : 0);
				_device->SetSamplerState(i, D3DSAMP_ADDRESSU, state.repeatX ? D3DTADDRESS_WRAP : D3DTADDRESS_CLAMP);
				_device->SetSamplerState(i, D3DSAMP_ADDRESSV, state.repeatY ? D3DTADDRESS_WRAP : D3DTADDRESS_CLAMP);
			}

			drawBuffer(buffer);
		}

		state.material->end();
	}

	delete &buffer;
	_drawBuffers.erase(_drawBuffers.begin() + bufferIndex);
}
void kGraphicsDX::getMatrixDX(const kMat3x3& mat, D3DXMATRIX* matdx) {
	matdx->m[0][0] = mat.m[0][0];
	matdx->m[0][1] = mat.m[0][1];
	matdx->m[0][2] = 0.0f;
	matdx->m[0][3] = mat.m[0][2];
	matdx->m[1][0] = mat.m[1][0];
	matdx->m[1][1] = mat.m[1][1];
	matdx->m[1][2] = 0.0f;
	matdx->m[1][3] = mat.m[1][2];
	matdx->m[2][0] = 0.0f;
	matdx->m[2][1] = 0.0f;
	matdx->m[2][2] = 1.0f;
	matdx->m[2][3] = 0.0f;
	matdx->m[3][0] = mat.m[2][0];
	matdx->m[3][1] = mat.m[2][1];
	matdx->m[3][2] = 0.0f;
	matdx->m[3][3] = mat.m[2][2];
}
kWindow* kGraphicsDX::getWindow() {
	return _window;
}
kEffect* kGraphicsDX::createEffect(kStream* source) {
	kEffectCg* effect = new kEffectCg(getEngine(), _cgContext, source);

	for (size_t i = 0; i < ES_Count; i++) {
		effect->addSemantic(i, _semanticNames[i]);
	}

	return effect;
}
void kGraphicsDX::deleteEffect(kEffect* effect) {
	delete effect;
}
void kGraphicsDX::flush(const kMaterial* material) {
	for (unsigned i = 0; i < _drawBuffers.size(); i++) {
		if ((!material) || (_drawBuffers[i]->state.material == material)) {
			flush(i--);
		}
	}
}
